/*
Copyright (C) 2011 The University of Michigan

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

Please send inquiries to powertutor@umich.edu
 */

package vn.cybersoft.obs.andriod.batterystats2.components;

import android.content.Context;
import android.location.GpsSatellite;
import android.location.GpsStatus;
import android.location.LocationManager;
import android.os.Build;
import android.os.SystemClock;
import android.util.Log;
import android.util.SparseArray;

import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Map;

import vn.cybersoft.obs.andriod.batterystats2.PowerNotifications;
import vn.cybersoft.obs.andriod.batterystats2.phone.PhoneConstants;
import vn.cybersoft.obs.andriod.batterystats2.service.IterationData;
import vn.cybersoft.obs.andriod.batterystats2.service.PowerData;
import vn.cybersoft.obs.andriod.batterystats2.util.NotificationService;
import vn.cybersoft.obs.andriod.batterystats2.util.Recycler;
import vn.cybersoft.obs.andriod.batterystats2.util.SystemInfo;

public class GPS extends PowerComponent {
	public static class GpsData extends PowerData {
		private static Recycler<GpsData> recycler = new Recycler<GpsData>();

		public static GpsData obtain() {
			GpsData result = recycler.obtain();
			if (result != null)
				return result;
			return new GpsData();
		}

		/* The time in seconds since the last iteration of data. */
		public double[] stateTimes;
		/*
		 * The number of satellites. This number is only available while the GPS
		 * is in the on state. Otherwise it is 0.
		 */
		public int satellites;

		private GpsData() {
			stateTimes = new double[GPS.POWER_STATES];
		}

		public void init(double[] stateTimes, int satellites) {
			for (int i = 0; i < GPS.POWER_STATES; i++) {
				this.stateTimes[i] = stateTimes[i];
			}
			this.satellites = satellites;
		}

		@Override
		public void recycle() {
			recycler.recycle(this);
		}

		@Override
		public void writeLogDataInfo(OutputStreamWriter out) throws IOException {
			StringBuilder res = new StringBuilder();
			res.append("GPS-state-times");
			for (int i = 0; i < GPS.POWER_STATES; i++) {
				res.append(" ").append(stateTimes[i]);
			}
			res.append("\nGPS-sattelites ").append(satellites).append("\n");
			out.write(res.toString());
		}
	}

	public static final int POWER_STATES = 3;
	public static final int POWER_STATE_OFF = 0;
	public static final int POWER_STATE_SLEEP = 1;
	public static final int POWER_STATE_ON = 2;
	public static final String[] POWER_STATE_NAMES = { "OFF", "SLEEP", "ON" };

	private static final String TAG = "GPS";

	private static final int HOOK_LIBGPS = 1;
	private static final int HOOK_GPS_STATUS_LISTENER = 2;
	private static final int HOOK_NOTIFICATIONS = 4;
	private static final int HOOK_TIMER = 8;

	/* A named pipe written to by the hacked libgps library. */
	private static String HOOK_GPS_STATUS_FILE = "/data/misc/gps.status";

	private GpsStatus.Listener gpsListener;
	private Thread statusThread;
	private PowerNotifications notificationReceiver;

	private Context context;
	private LocationManager locationManager;
	private GpsStatus lastStatus;
	private boolean hasUidInfo;
	private long sleepTime;
	private long lastTime;

	private GpsStateKeeper gpsState;
	private SparseArray<GpsStateKeeper> uidStates;

	private static final int GPS_STATUS_SESSION_BEGIN = 1;
	private static final int GPS_STATUS_SESSION_END = 2;
	private static final int GPS_STATUS_ENGINE_ON = 3;
	private static final int GPS_STATUS_ENGINE_OFF = 4;

	public GPS(Context context, PhoneConstants constants) {
		this.context = context;
		uidStates = new SparseArray<GpsStateKeeper>();
		sleepTime = (long) Math.round(1000.0 * constants.gpsSleepTime());

		hasUidInfo = NotificationService.available();

		int hookMethod = 0;
		final File gpsStatusFile = new File(HOOK_GPS_STATUS_FILE);
		if (gpsStatusFile.exists()) {
			/*
			 * The libgps hack appears to be available. Let's use this to gather
			 * our status updates from the GPS.
			 */
			hookMethod = HOOK_LIBGPS;
		} else {
			/*
			 * We can always use the status listener hook and perhaps the
			 * notification hook if we are running eclaire or higher and the
			 * notification hook is installed. We can only do this on eclaire or
			 * higher because it wasn't until eclaire that they fixed a bug
			 * where they didn't maintain a wakelock while the gps engine was
			 * on.
			 */
			hookMethod = HOOK_GPS_STATUS_LISTENER;
			try {
				if (NotificationService.available()
						&& Integer.parseInt(Build.VERSION.SDK) >= 5 /*
																	 * eclaire
																	 * or higher
																	 */) {
					hookMethod |= HOOK_NOTIFICATIONS;
				}
			} catch (NumberFormatException e) {
				Log.w(TAG, "Could not parse sdk version: " + Build.VERSION.SDK);
			}
		}
		/*
		 * If we don't have a way of getting the off<->sleep transitions through
		 * notifications let's just use a timer and simulat the state of the gps
		 * instead.
		 */
		if ((hookMethod & (HOOK_LIBGPS | HOOK_NOTIFICATIONS)) == 0) {
			hookMethod |= HOOK_TIMER;
		}

		/* Create the object that keeps track of the physical GPS state. */
		gpsState = new GpsStateKeeper(hookMethod, sleepTime);

		/*
		 * No matter what we are going to register a GpsStatus listener so that
		 * we can get the satellite count. Also if anything goes wrong with the
		 * libgps hook we will revert to using this.
		 */
		locationManager = (LocationManager) context
				.getSystemService(Context.LOCATION_SERVICE);
		gpsListener = new GpsStatus.Listener() {
			public void onGpsStatusChanged(int event) {
				if (event == GpsStatus.GPS_EVENT_STARTED) {
					gpsState.updateEvent(GPS_STATUS_SESSION_BEGIN,
							HOOK_GPS_STATUS_LISTENER);
				} else if (event == GpsStatus.GPS_EVENT_STOPPED) {
					gpsState.updateEvent(GPS_STATUS_SESSION_END,
							HOOK_GPS_STATUS_LISTENER);
				}
				synchronized (GPS.this) {
					lastStatus = locationManager.getGpsStatus(lastStatus);
				}
			}
		};
		locationManager.addGpsStatusListener(gpsListener);

		/*
		 * No matter what we register a notification service listener as well so
		 * that we can get uid information if it's available.
		 */
		if (hasUidInfo) {
			notificationReceiver = new NotificationService.DefaultReceiver() {
				public void noteStartWakelock(int uid, String name, int type) {
					if (uid == SystemInfo.AID_SYSTEM
							&& "GpsLocationProvider".equals(name)) {
						gpsState.updateEvent(GPS_STATUS_ENGINE_ON,
								HOOK_NOTIFICATIONS);
					}
				}

				public void noteStopWakelock(int uid, String name, int type) {
					if (uid == SystemInfo.AID_SYSTEM
							&& "GpsLocationProvider".equals(name)) {
						gpsState.updateEvent(GPS_STATUS_ENGINE_OFF,
								HOOK_NOTIFICATIONS);
					}
				}

				public void noteStartGps(int uid) {
					updateUidEvent(uid, GPS_STATUS_SESSION_BEGIN,
							HOOK_NOTIFICATIONS);
				}

				public void noteStopGps(int uid) {
					updateUidEvent(uid, GPS_STATUS_SESSION_END,
							HOOK_NOTIFICATIONS);
				}
			};
			NotificationService.addHook(notificationReceiver);
		}

		if (gpsStatusFile.exists()) {
			/*
			 * Start a thread to read from the named pipe and feed us status
			 * updates.
			 */
			statusThread = new Thread() {
				public void run() {
					try {
						java.io.FileInputStream fin = new java.io.FileInputStream(
								gpsStatusFile);
						for (int event = fin.read(); !interrupted()
								&& event != -1; event = fin.read()) {
							gpsState.updateEvent(event, HOOK_LIBGPS);
						}
					} catch (IOException e) {
						e.printStackTrace();
					}
					if (!interrupted()) {
						// TODO: Have this instead just switch to use different
						// hooks.
						Log.w(TAG, "GPS status thread exited. "
								+ "No longer gathering gps data.");
					}
				}
			};
			statusThread.start();
		}
	}

	private void updateUidEvent(int uid, int event, int source) {
		synchronized (uidStates) {
			GpsStateKeeper state = uidStates.get(uid);
			if (state == null) {
				state = new GpsStateKeeper(HOOK_NOTIFICATIONS | HOOK_TIMER,
						sleepTime, lastTime);
				uidStates.put(uid, state);
			}
			state.updateEvent(event, source);
		}
	}

	@Override
	protected void onExit() {
		if (gpsListener != null) {
			locationManager.removeGpsStatusListener(gpsListener);
		}
		if (statusThread != null) {
			statusThread.interrupt();
		}
		if (notificationReceiver != null) {
			NotificationService.removeHook(notificationReceiver);
		}
		super.onExit();
	}

	@Override
	public IterationData calculateIteration(long iteration) {
		IterationData result = IterationData.obtain();

		/* Get the number of satellites that were available in the last update. */
		int satellites = 0;
		synchronized (this) {
			if (lastStatus != null) {
				for (GpsSatellite satellite : lastStatus.getSatellites()) {
					satellites++;
				}
			}
		}

		/* Get the power data for the physical gps device. */
		GpsData power = GpsData.obtain();
		synchronized (gpsState) {
			double[] stateTimes = gpsState.getStateTimesLocked();
			int curState = gpsState.getCurrentStateLocked();
			power.init(stateTimes, curState == POWER_STATE_ON ? satellites : 0);
			gpsState.resetTimesLocked();
		}
		result.setPowerData(power);

		/* Get the power data for each uid if we have information on it. */
		if (hasUidInfo)
			synchronized (uidStates) {
				lastTime = beginTime + iterationInterval * iteration;
				for (int i = 0; i < uidStates.size(); i++) {
					int uid = uidStates.keyAt(i);
					GpsStateKeeper state = uidStates.valueAt(i);

					double[] stateTimes = state.getStateTimesLocked();
					int curState = state.getCurrentStateLocked();
					GpsData uidPower = GpsData.obtain();
					uidPower.init(stateTimes,
							curState == POWER_STATE_ON ? satellites : 0);
					state.resetTimesLocked();

					result.addUidPowerData(uid, uidPower);

					/*
					 * Remove state information for uids no longer using the
					 * gps.
					 */
					if (curState == POWER_STATE_OFF) {
						uidStates.remove(uid);
						i--;
					}
				}
			}

		return result;
	}

	@Override
	public boolean hasUidInformation() {
		return hasUidInfo;
	}

	/*
	 * This class is used to maintain the actual GPS state in addition to
	 * simulating individual uid states.
	 */
	private static class GpsStateKeeper {
		private double[] stateTimes;
		private long lastTime;
		private int curState;

		/* The sum of whatever hook sources are valid. See the HOOK_ constants. */
		private int hookMask;
		/*
		 * The time that the GPS hardware should turn off. This is only used if
		 * HOOK_TIMER is in the hookMask.
		 */
		private long offTime;
		/*
		 * Gives the time that the GPS stays in the sleep state after the
		 * session has ended in milliseconds.
		 */
		private long sleepTime;

		public GpsStateKeeper(int hookMask, long sleepTime) {
			this(hookMask, sleepTime, SystemClock.elapsedRealtime());
		}

		public GpsStateKeeper(int hookMask, long sleepTime, long lastTime) {
			this.hookMask = hookMask;
			this.sleepTime = sleepTime; /*
										 * This isn't required if HOOK_TIEMR is
										 * not set.
										 */
			this.lastTime = lastTime;
			stateTimes = new double[POWER_STATES];
			curState = POWER_STATE_OFF;
			offTime = -1;
		}

		/* Make sure that you have a lock on this before calling. */
		public double[] getStateTimesLocked() {
			updateTimesLocked();

			/*
			 * Let's normalize the times so that power measurements are
			 * consistent.
			 */
			double total = 0;
			for (int i = 0; i < POWER_STATES; i++) {
				total += stateTimes[i];
			}
			if (total == 0)
				total = 1;
			for (int i = 0; i < POWER_STATES; i++) {
				stateTimes[i] /= total;
			}

			return stateTimes;
		}

		public void resetTimesLocked() {
			for (int i = 0; i < POWER_STATES; i++) {
				stateTimes[i] = 0;
			}
		}

		public int getCurrentStateLocked() {
			return curState;
		}

		/* Make sure that you have a lock on this before calling. */
		private void updateTimesLocked() {
			/* Update the time we were in the previous state. */
			long curTime = SystemClock.elapsedRealtime();

			/* Check if the GPS has gone to sleep as a result of a timer. */
			if ((hookMask & HOOK_TIMER) != 0 && offTime != -1
					&& offTime < curTime) {
				stateTimes[curState] += (offTime - lastTime) / 1000.0;
				curState = POWER_STATE_OFF;
				offTime = -1;
			}

			/* Update the amount of time that we've been in the current state. */
			stateTimes[curState] += (curTime - lastTime) / 1000.0;
			lastTime = curTime;
		}

		/*
		 * When a hook source gets an event it should report it to updateEvent.
		 * The only exception is HOOK_TIMER which is handled within this class
		 * itself.
		 */
		public void updateEvent(int event, int source) {
			synchronized (this) {
				if ((hookMask & source) == 0) {
					/* We are not using this hook source, ignore. */
					return;
				}

				updateTimesLocked();
				int oldState = curState;
				switch (event) {
				case GPS_STATUS_SESSION_BEGIN:
					curState = POWER_STATE_ON;
					break;
				case GPS_STATUS_SESSION_END:
					if (curState == POWER_STATE_ON) {
						curState = POWER_STATE_SLEEP;
					}
					break;
				case GPS_STATUS_ENGINE_ON:
					if (curState == POWER_STATE_OFF) {
						curState = POWER_STATE_SLEEP;
					}
					break;
				case GPS_STATUS_ENGINE_OFF:
					curState = POWER_STATE_OFF;
					break;
				default:
					Log.w(TAG, "Unknown GPS event captured");
				}
				if (curState != oldState) {
					if (oldState == POWER_STATE_ON
							&& curState == POWER_STATE_SLEEP) {
						offTime = SystemClock.elapsedRealtime() + sleepTime;
					} else {
						/*
						 * Any other state transition should reset the off
						 * timer.
						 */
						offTime = -1;
					}
				}
			}
		}
	}

	@Override
	public String getComponentName() {
		return "GPS";
	}
}
